/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daios.framework.structs;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.collections.iterators.ArrayIterator;

import at.ac.tuwien.infosys.dsg.daios.exceptions.ArrayException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.TypeErrorException;
import at.ac.tuwien.infosys.dsg.daios.wsdl.matcher.Matcher;

/**
 * 
 * A Daios-style message. Daios messages are simple
 * data structures, that mainly consist of a finite number of
 * name/value pairs. Names are always of type java.lang.String,
 * while values can be any of:
 * 
 * <pre>
 *   -- java.lang.String
 *   -- java.lang.Integer
 *   -- java.lang.Long
 *   -- java.lang.Short
 *   -- java.lang.Byte[]
 *   -- java.lang.Float
 *   -- java.lang.Double
 *   -- java.lang.Boolean
 * </pre>
 * 
 * or an array of each of those types. To constuct more complex data
 * structures Daios messages can be built recursively, i.e. values can
 * also be other Daios messages.
 * 
 * <p>
 * 
 * Every Daios message has a <i>structure</i> or signature. The structure
 * of a message consists of the name and type of each <i>field</i>.<br>
 * 
 * Example:
 * 
 * <pre>
 *   name:java.lang.String
 *   address:
 *     street:java.lang.String
 *     postal:java.lang.String
 *     city:java.lang.String
 *     nr:java.lang.Integer
 *     door:java.lang.Short
 * </pre>
 * 
 * or 
 * 
 * <pre>
 *   numbers:java.lang.Long[]
 * </pre>
 * 
 * 
 * @author philipp
 *
 */
public class DaiosMessage {
	
	/**
	 * This constant defines "infinite distance".
	 */
	public static final int MAX_DISTANCE =
		Matcher.MAX_DISTANCE;
	
	// the message contents
	private Hashtable<String,Object> content = null;
	// the message types
	private Hashtable<String,Class> types = null;
	
	/**
	 * The default constructor. Create a new empty Daios message.
	 *
	 */
	public DaiosMessage() {
		content = new Hashtable<String,Object>();
		types = new Hashtable<String,Class>();
	}
	
	/**
	 * Get the names of all fields in this message.
	 * 
	 * @return The names of all fields.
	 */
	public Enumeration<String> getNames() {
		return content.keys();
	}
	
	/**
	 * Get the types of all fields in this message.
	 * 
	 * @return The types of all fields.
	 */
	public Enumeration<Class> getTypes() {
		return types.elements();
	}
	
	/**
	 * Add an object with a know type to  the 
	 * message. The type of the message is given
	 * as java.lang.Class.
	 * 
	 * @param key The name of the new field.
	 * @param value The value of the new field.
	 * @param c The Java class of the new field.
	 * 
	 * @throws TypeErrorException
	 */
	public void set(String key, Object value, Class c)
		throws TypeErrorException, ArrayException {

		if(!c.isInstance(value))
			throw new TypeErrorException(key,c,value.getClass());		
		
		if(c.equals(DaiosMessage[].class))
			if(!checkArrayConsistency((DaiosMessage[])value))
				throw new ArrayException("Elements of complex array differ in "+
					"structure");
				
			
		content.put(key,value);
		types.put(key,c);		
	}
	
	/**
	 * Add an object of undefined type as field to the
	 * message.
	 * 
	 * @param key The name of the new field
	 * @param value The value of the new field
	 * @throws ArrayException 
	 */
	public void set(String key, Object value) throws ArrayException {

		Class c = value.getClass();
		
		if(c.equals(DaiosMessage[].class))
			if(!checkArrayConsistency((DaiosMessage[])value))
				throw new ArrayException("Elements of complex array differ in "+
					"structure");
		
		content.put(key,value);
		types.put(key,c);
	}
	
	/**
	 * Get a field value with unknown type from the
	 * message.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */
	public Object get(String key) {
		return content.get(key);
	}
	
	/**
	 * Add an object of type Integer as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Integer value of the new field.
	 */
	public void setInt(String key, Integer val) {
		content.put(key,val);
		types.put(key,Integer.class);
	}
	
	/**
	 * Get a field value with type Integer from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */
	public Integer getInt(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Integer.class.equals(types.get(key)))
			throw new TypeErrorException(key,Integer.class,types.get(key));		
		return (Integer)content.get(key);
	}

	/**
	 * Add an object of type Array of Integer as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Integer[] value of the new field.
	 */	
	public void setIntArray(String key, Integer[] val) {
		content.put(key,val);
		types.put(key,Integer[].class);
	}

	/**
	 * Get a field value with type Array of Integer from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */
	public Integer[] getIntArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Integer[].class.equals(types.get(key)))
			throw new TypeErrorException(key,Integer[].class,types.get(key));		
		return (Integer[])content.get(key);
	}
	
	/**
	 * Add an object of type Short as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Short value of the new field.
	 */	
	public void setShort(String key, Short val) {
		content.put(key,val);
		types.put(key,Short.class);
	}

	/**
	 * Get a field value with type Short from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */
	public Short getShort(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Short.class.equals(types.get(key)))
			throw new TypeErrorException(key,Short.class,types.get(key));		
		return (Short)content.get(key);
	}
	
	/**
	 * Add an object of type Array of Short as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Short[] value of the new field.
	 */	
	public void setShortArray(String key, Short[] val) {
		content.put(key,val);
		types.put(key,Short[].class);
	}

	/**
	 * Get a field value with type Array of Short from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */
	public Short[] getShortArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Short[].class.equals(types.get(key)))
			throw new TypeErrorException(key,Short[].class,types.get(key));		
		return (Short[])content.get(key);
	}

	/**
	 * Add an object of type Long as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Long value of the new field.
	 */		
	public void setLong(String key, Long val) {
		content.put(key,val);
		types.put(key,Long.class);
	}

	/**
	 * Get a field value with type Long from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */	
	public Long getLong(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Long.class.equals(types.get(key)))
			throw new TypeErrorException(key,Long.class,types.get(key));		
		return (Long)content.get(key);
	}

	/**
	 * Add an object of type Array of Long as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Long[] value of the new field.
	 */		
	public void setLongArray(String key, Long[] val) {
		content.put(key,val);
		types.put(key,Long[].class);
	}

	/**
	 * Get a field value with type Array of Long from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */		
	public Long[] getLongArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Long[].class.equals(types.get(key)))
			throw new TypeErrorException(key,Long[].class,types.get(key));		
		return (Long[])content.get(key);
	}
	
	/**
	 * Add an object of type Float as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Float value of the new field.
	 */			
	public void setFloat(String key, Float val) {
		content.put(key,val);
		types.put(key,Float.class);
	}

	/**
	 * Get a field value with type Float from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */		
	public Float getFloat(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Float.class.equals(types.get(key)))
			throw new TypeErrorException(key,Float.class,types.get(key));		
		return (Float)content.get(key);
	}

	/**
	 * Add an object of type Array of Float as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Float[] value of the new field.
	 */	
	public void setFloatArray(String key, Float[] val) {
		content.put(key,val);
		types.put(key,Float[].class);
	}

	/**
	 * Get a field value with type Array of Float from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */			
	public Float[] getFloatArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Float[].class.equals(types.get(key)))
			throw new TypeErrorException(key,Float[].class,types.get(key));		
		return (Float[])content.get(key);
	}
	
	/**
	 * Add an object of type Double as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Double value of the new field.
	 */		
	public void setDouble(String key, Double val) {
		content.put(key,val);
		types.put(key,Double.class);
	}

	/**
	 * Get a field value with type Double from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */			
	public Double getDouble(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Double.class.equals(types.get(key)))
			throw new TypeErrorException(key,Double.class,types.get(key));		
		return (Double)content.get(key);
	}

	/**
	 * Add an object of type Array of Double as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Double[] value of the new field.
	 */			
	public void setDoubleArray(String key, Double[] val) {
		content.put(key,val);
		types.put(key,Double[].class);
	}

	/**
	 * Get a field value with type Array of Double from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */		
	public Double[] getDoubleArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Double[].class.equals(types.get(key)))
			throw new TypeErrorException(key,Double[].class,types.get(key));		
		return (Double[])content.get(key);
	}
	
	/**
	 * Add an object of type Array of Byte as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Byte[] value of the new field.
	 */	
	public void setByteArray(String key, Byte[] val) {
		content.put(key,val);
		types.put(key,Byte[].class);
	}

	/**
	 * Get a field value with type Array of Byte from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */
	public Byte[] getByteArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Byte[].class.equals(types.get(key)))
			throw new TypeErrorException(key,Byte[].class,types.get(key));		
		return (Byte[])content.get(key);
	}

	/**
	 * Add an object of type String as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The String value of the new field.
	 */
	public void setString(String key, String val) {
		content.put(key,val);
		types.put(key, String.class);
	}

	/**
	 * Get a field value with type String from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */	
	public String getString(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!String.class.equals(types.get(key)))
			throw new TypeErrorException(key,String.class,types.get(key));		
		return (String)content.get(key);
	}

	/**
	 * Add an object of type Array of String as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The String[] value of the new field.
	 */
	public void setStringArray(String key, String[] val) {
		content.put(key,val);
		types.put(key,String[].class);
	}

	/**
	 * Get a field value with type Array of String from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */	
	public String[] getStringArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!String[].class.equals(types.get(key)))
			throw new TypeErrorException(key,String[].class,types.get(key));		
		return (String[])content.get(key);
	}

	/**
	 * Add an object of type Boolean as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Boolean value of the new field.
	 */	
	public void setBoolean(String key, Boolean val) {
		content.put(key, val);
		types.put(key, Boolean.class);
	}
	
	/**
	 * Get a field value with type Boolean from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */		
	public Boolean getBoolean(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Boolean.class.equals(types.get(key)))
			throw new TypeErrorException(key,Boolean.class,types.get(key));		
		return (Boolean)content.get(key);		
	}

	/**
	 * Add an object of type Array of Boolean as field to the message.
	 * 
	 * @param key The name of the new field.
	 * @param val The Boolean value of the new field.
	 */		
	public void setBooleanArray(String key, Boolean[] val) {
		content.put(key,val);
		types.put(key,Boolean[].class);
	}

	/**
	 * Get a field value with type Array of Boolean from the
	 * message. If the field exists but has any other type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */		
	public Boolean[] getBooleanArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!Boolean[].class.equals(types.get(key)))
			throw new TypeErrorException(key,Boolean[].class,types.get(key));		
		return (Boolean[])content.get(key);
	}	
	
	/**
	 * Add an object of type DaiosMessage as field to the message.
	 * This method is used to construct complex data types.
	 * 
	 * @param key The name of the new field.
	 * @param val The DaiosMessage value of the new field.
	 */	
	public void setComplex(String key, DaiosMessage val) {
		content.put(key,val);
		types.put(key,DaiosMessage.class);
	}

	/**
	 * Get a field value with type DaiosMessage from the
	 * message. If the field exists but has a basic type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */	
	public DaiosMessage getComplex(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!DaiosMessage.class.equals(types.get(key)))
			throw new TypeErrorException(key,DaiosMessage.class,types.get(key));		
		return (DaiosMessage)content.get(key);
	}

	/**
	 * Add an object of type Array of DaiosMessage as field to the message.
	 * This method is used to construct complex data types.
	 * 
	 * @param key The name of the new field.
	 * @param val The DaiosMessage[] value of the new field.
	 */		
	public void setComplexArray(String key, DaiosMessage[] val) 
		throws ArrayException {
		if(!checkArrayConsistency(val))
			throw new ArrayException("Elements of complex array differ in "+
				"structure");
		content.put(key,val);
		types.put(key,DaiosMessage[].class);
	}

	/**
	 * Get a field value with type Array of DaiosMessage from the
	 * message. If the field exists but has a basic type
	 * a TypeErrorException will be thrown.
	 * 
	 * @param key The name of the field.
	 * @return The value of the field.
	 */		
	public DaiosMessage[] getComplexArray(String key) throws TypeErrorException {
		if(types.get(key) == null)
			return null;
		if(!DaiosMessage[].class.equals(types.get(key)))
			throw new TypeErrorException(key,DaiosMessage[].class,types.get(key));
		return (DaiosMessage[])content.get(key);
	}
	
	/**
	 * Remove  a field from the message.
	 * 
	 * @param key The name of the field to remove.
	 * @return True if the field has been successfully removed,
	 * false otherwise. The method may for example return false
	 * if the field does nto exist in the message.
	 */
	public boolean remove(String key) {
		return (content.remove(key) != null)
			& (types.remove(key) != null);
	}
	
	/**
	 * Get the number of fields contained in this message.
	 * 
	 * @return The number of fields.
	 */
	public int getNrOfFields() {
		return content.size();
	}
	
	/**
	 * Check if a given field in the message has simple
	 * type (i.e. is not DaiosMessage).
	 * 
	 * @param name The name of th field to check.
	 * @return True if the field is simple, false
	 * otherwise.
	 */
	public boolean isSimple(String name) {
		
		if(types.get(name) == null)
			return false;
		
		return !(types.get(name).equals(DaiosMessage.class))
				&& !(types.get(name).equals(DaiosMessage[].class));
	}

	/**
	 * Check if a given field in the message is an array
	 * type. Note that Byte[] is treated as a simple, non-array
	 * type since it is encoded as base64binary as soon as it is
	 * on the wire.
	 * 
	 * @param name The name of the field to check.
	 * @return True if the field is an array, false
	 * otherwise.
	 */	
	public boolean isArray(String name) {
		
		if(types.get(name) == null)
			return false;
		
		// special case: we consider a byte[]
		// as a simple type
		if(types.get(name).equals(Byte[].class))
			return false;
		
		return types.get(name).isArray();
		
	}
	
	/**
	 * Checks if this message is structurally equal
	 * to another DaiosMessage. Messages are
	 * considered to be <i>structurally equal</i>
	 * if they have a <i>distance</i> of 0.
	 * 
	 * @param typeStructure  The other message to
	 * compare against this one.
	 * 
	 * @return True if the messages are structurally
	 * equal. false otherwise.
	 */
	public boolean structuralEqual(DaiosMessage typeStructure) {
		
		return structuralDistance(typeStructure) == 0;

	}	
	
	/**
	 * Calculates the <i>distance</i> between this message and
	 * another one. The distance of two Daios messages is defined
	 * as:
	 * 
	 * <pre>
	 *   TODO
	 * </pre>
	 * 
	 * Note that this distance calculation is not symmetric, i.e.
	 * distance(a,b) is not necessary equal to distance(b,a). A special
	 * case is <i>structural equality</i>, i.e. distance(a,b) = 0. In
	 * that case distance(a,b) is equal to distance(b,a).
	 * 
	 * @param otherMsg The message to compare against this message.
	 * @return The distance of the two messages.
	 */
	public int structuralDistance(DaiosMessage otherMsg) {
		
		// we consider 'null' to be an empty message
		if(otherMsg == null) {
			if(getNrOfFields() == 0)
				return 0;
			else
				return MAX_DISTANCE;
		}
		
		int distance = 0;
		List<String> used = new LinkedList<String>();
		
		Enumeration<String> names = getNames();
		String name = null;
		Object val = null;
		
		// for each field in this message
		while(names.hasMoreElements()) {
			name = names.nextElement();
			// is this field contained in the other message struc ?
			if((val = otherMsg.get(name)) == null)
				return MAX_DISTANCE;
			// is it a simple type ?
			if(!(val instanceof DaiosMessage) && !(val instanceof DaiosMessage[])) {
				if(!val.getClass().equals(get(name).getClass()))
					return MAX_DISTANCE;
			}
			// or a complex type - use recursion to calculate the
			// distance of this 'subtree' of the structure
			else if(val instanceof DaiosMessage) {
				try {
					int thisDist = 
						((DaiosMessage)val).structuralDistance((DaiosMessage)get(name));
					if(thisDist == MAX_DISTANCE)
						return MAX_DISTANCE;
					else
						distance += thisDist;
				} catch(ClassCastException e) {
					return MAX_DISTANCE;
				}
			}
			// or an array of complex types
			else {
				// in that case we compare the first elements from both arrays
				try {
					int thisDist = 
						((DaiosMessage[])val)[0]
						  .structuralDistance(((DaiosMessage[])get(name))[0]);
					if(thisDist == MAX_DISTANCE)
						return MAX_DISTANCE;
					else
						distance += thisDist;
				} catch(ClassCastException e) {
					return MAX_DISTANCE;
				}				
			}
			
			// add to the list of used names
			used.add(name);
		}			
		
		// check how many names from the other message have not been used
		Enumeration<String> otherNames = otherMsg.getNames();
		int notUsed = 0;
		String otherName = null;
		while(otherNames.hasMoreElements()) {
			otherName = otherNames.nextElement();
			if(!used.contains(otherName))
				notUsed++;
		}
		
		// the distance is the number of fields that have not been used PLUS the
		// sum of the distances that have been calculated in the various subtrees 
		return notUsed+distance;
		
	}
	
	/**
	 * Check if the given DaiosMessage array is consistent, i.e.
	 * whether all messages in the array have the same structure.
	 * 
	 * @param val The array to check for consistency.
	 * @return True if the array is consistent, false otherwise.
	 */
	private static boolean checkArrayConsistency(DaiosMessage[] val) {
		
		if(val.length < 2)
			return true;
		
		// our comparation structure
		DaiosMessage cp = val[0];
		// check for all other members whether they match the first
		// element
		for(int i=1; i<val.length; i++) {
			if(!val[i].structuralEqual(cp))
				return false;
		}
		
		return true;
		
	}
	
	/**
	 * 
	 * Print this DaiosMessage.
	 * 
	 */
	public String toString() {
		return this.toString(0);
	}
	
	/**
	 * Pretty-print the message with all fields and structure.
	 * 
	 * @param tabs 
	 * @return A String containing a String-representation of the
	 * message.
	 */
	private String toString(int tabs) {
		
		StringBuffer sb = new StringBuffer();
		Enumeration<String> conts = content.keys();
		
		String s = null;
		while(conts.hasMoreElements()) {
			for(int i=0; i<tabs; i++)
				sb.append("  ");
			s = conts.nextElement();
			sb.append(s);
			sb.append(" : ");
			sb.append(types.get(s).getCanonicalName());
			sb.append(" : ");
			if(content.get(s) instanceof DaiosMessage) {
				sb.append("\n");
				sb.append(((DaiosMessage)content.get(s)).toString(tabs+1));
			}
			else if(types.get(s).isArray()) {
				ArrayIterator it = new ArrayIterator(content.get(s));
				Object o = null;
				while(it.hasNext()) {
					o = it.next();
					if(o instanceof DaiosMessage) {
						sb.append("\n");
						sb.append(((DaiosMessage)o).toString(tabs+1));
					}
					else {
						sb.append(o);
						sb.append(" ");
					}
				}
			}
			else
				sb.append(content.get(s));
			sb.append("\n");
		}
		
		return sb.toString();
		
	}
}
